package com.example.deepschool.service.CourseResource;

import com.example.deepschool.Model.Course;
import com.example.deepschool.Model.Order;
import com.example.deepschool.Model.User;
import com.example.deepschool.enums.GlobalVariables_String;
import com.example.deepschool.exception.CustomException;
import com.example.deepschool.repositories.CourseRepository;
import com.example.deepschool.repositories.CourseResourceRepository;
import com.example.deepschool.repositories.OrderRepository;
import com.example.deepschool.repositories.UserCourseRepository;
import com.example.deepschool.util.*;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;

@Service
public class CourseResourceImpl implements CourseResource {
	
	private final CourseResourceRepository courseResourceRepository;
	private final UserCourseRepository userCourseRepository;
	private final CourseRepository courseRepository;
	private final OrderRepository orderRepository;
	
	public CourseResourceImpl (CourseResourceRepository courseResourceRepository,
	                           UserCourseRepository userCourseRepository,
	                           CourseRepository courseRepository,
	                           OrderRepository orderRepository) {
		this.courseResourceRepository = courseResourceRepository;
		this.userCourseRepository = userCourseRepository;
		this.courseRepository = courseRepository;
		this.orderRepository = orderRepository;
	}
	
	/**
	 * 验证用户是否为指定课程的管理员。
	 * <p>
	 * 此方法首先从Redis中根据用户UUID获取用户信息，然后检查该用户是否有修改所属课程的权限。
	 * 接着，从数据库中获取指定课程的所有管理员用户ID，并验证当前用户是否在这些管理员ID之中。
	 * 如果用户是课程管理员，则返回该用户对象；否则，抛出相应的运行时异常。
	 *
	 * @param courseID 课程的唯一标识ID，类型为Long。
	 * @param UUID     用户的唯一标识UUID，类型为String。
	 * @return 如果验证通过，返回表示用户的{@link User}对象。
	 * @throws RuntimeException 当用户权限不足或不属于该课程的管理者时抛出。
	 */
	private User VerifyCourseMaster (Long courseID, String UUID) {
		// 从Redis中根据UUID获取用户信息
		User user = User.fromJson(Redis.HGET(Redis.GET(UUID), "user"));
		
		// 验证用户是否有修改课程的权限
		if (! PermissionsControl.VERIFY_MODIFY_OWN_COURSE(user.getPermissions())) {
			throw new RuntimeException("权限不足");
		}
		
		// 查询指定课程的所有管理员用户ID
		Long[] userIds = this.userCourseRepository.selectUserIds(courseID);
		
		// 验证当前用户是否为课程管理员
		boolean isCourseMaster = false;
		for (Long userId : userIds) {
			if (userId.equals(user.getId())) {
				isCourseMaster = true;
				break;
			}
		}
		
		// 若用户不是课程管理员，则抛出异常
		if (! isCourseMaster) {
			throw new RuntimeException("你不属于该课程的管理者");
		}
		
		// 验证通过，返回用户对象
		return user;
	}
	
	
	/**
	 * 验证用户是否拥有指定课程的访问权限。
	 * <p>
	 * 方法首先从Redis中根据UUID获取用户信息，然后查询指定课程的详细信息，
	 * 包括判断课程是否为开放资源。接着，检查用户是否为该课程的管理员。
	 * 如果课程非开放资源且用户非管理员，则抛出异常。最后，验证用户是否已购买课程，
	 * 若未购买且非管理员，同样抛出异常。
	 *
	 * @param courseId 课程ID，用于识别目标课程，类型为Long。
	 * @param UUID     用户的唯一标识UUID，用于检索用户信息，类型为String。
	 * @throws RuntimeException 当课程未开放资源且用户非管理员，或用户未购买课程且非管理员时抛出。
	 */
	private void isOwnedCourse (Long courseId, String UUID) {
		// 从Redis中获取用户信息
		User user = User.fromJson(Redis.HGET(Redis.GET(UUID), "user"));
		
		// 查询课程详情，包括是否为开放资源
		Course course = this.courseRepository.select(courseId);
		boolean isOpenResource = course.getIsOpenResource() == 1;
		
		// 检查用户是否为课程管理员
		Long[] userIds = this.userCourseRepository.selectUserIds(courseId);
		boolean isCourseMaster = false;
		for (Long userId : userIds) {
			if (userId.equals(user.getId())) {
				isCourseMaster = true;
				break;
			}
		}
		
		// 若课程非开放资源且用户非管理员，抛出异常
		if (! isOpenResource && ! isCourseMaster) {
			throw new RuntimeException("该课程未开放资源");
		}
		
		// 创建订单实例以查询用户是否已购买课程
		Order order = new Order();
		order.setUserId(user.getId());
		order.setCourseId(courseId);
		
		// 验证用户是否已购买课程，若未购买且非管理员，抛出异常
		if (this.orderRepository.selectByUserIdAndCourseId(order) == null && ! isCourseMaster) {
			throw new RuntimeException("请先购买课程");
		}
	}
	
	/**
	 * 创建课程资源记录并更新结果映射。
	 * <p>
	 * 本方法接收资源名称、课程ID及资源地址，从中提取资源类型，
	 * 并利用这些信息创建一个新的课程资源数据库记录。随后，
	 * 它更新传入的Map以指示操作已完成，同时附上资源的地址和类型信息。
	 *
	 * @param ResourceName 资源的名称，类型为String。
	 * @param courseID     课程的ID，与资源关联，类型为Long。
	 * @param address      资源的存储地址，从中可解析出资源类型，类型为String。
	 * @param map          用于记录操作状态和详细信息的Map，将被更新并返回，类型为Map<String, Object>。
	 * @return 返回更新后的Map，含有"isComplete"、"address"和"type"键，分别表示操作状态、资源地址和资源类型。
	 */
	private Map<String, Object> getStringObjectMap (String ResourceName, Long courseID, String address, Map<String, Object> map) {
		// 解析资源地址以确定资源类型
		String type = address.split("/")[0];
		
		// 使用提供的信息创建课程资源对象并存储到数据库
		com.example.deepschool.Model.CourseResource courseResource =
				new com.example.deepschool.Model.CourseResource(courseID, ResourceName, address, type);
		this.courseResourceRepository.insert(courseResource);
		
		// 更新结果Map，确认操作完成，并添加资源地址和类型信息
		map.put("isComplete", true);
		map.put("address", address);
		map.put("type", type);
		
		// 返回更新后的结果Map
		return map;
	}
	
	
	/**
	 * 检查资源是否存在并处理相关逻辑。
	 * <p>
	 * 此方法首先验证用户是否为指定课程的管理员，然后根据文件的SHA256校验和检查文件是否存在。
	 * 如果文件存在，会将资源信息保存至数据库，并返回资源地址及类型；
	 * 若文件不存在，则返回当前已上传的块数量。过程中发生的任何异常会被转换为自定义异常。
	 *
	 * @param intactSHA256 文件的SHA256校验和，用于唯一标识文件，类型为String。
	 * @param ResourceName 资源的名称，类型为String。
	 * @param courseID     关联的课程ID，类型为Long。
	 * @param UUID         用户的唯一标识UUID，用于权限验证，类型为String。
	 * @return 返回一个Map，包含资源是否存在标志("isComplete")、资源地址("address")、资源类型("type")或已上传块数("ChunkNum")。
	 */
	@SneakyThrows
	@Override
	public Map<String, Object> isExist (String intactSHA256, String ResourceName, Long courseID, String UUID) {
		try {
			// 验证用户是否为课程管理员
			User user = VerifyCourseMaster(courseID, UUID);
			
			// 计算已上传的文件块数量
			Long ChunkNum = Redis.LLEN(intactSHA256);
			
			// 验证文件是否存在，并获取其地址
			String address = Verify.VerifyFileIsExistCourseResource(intactSHA256);
			
			// 准备结果Map
			Map<String, Object> resultMap = new HashMap<>();
			
			// 如果文件存在
			if (address != null) {
				return getStringObjectMap(ResourceName, courseID, address, new HashMap<>());
			}
			
			// 文件不存在时的响应
			resultMap.put("isComplete", false);
			resultMap.put("ChunkNum", ChunkNum);
			return resultMap;
		} catch (Exception e) {
			// 转换并抛出自定义异常
			throw new CustomException(400, e.getMessage());
		}
	}
	
	/**
	 * 删除指定课程资源。
	 * <p>
	 * 首先验证用户是否为课程管理员，然后尝试根据ID获取课程资源。
	 * 如果资源存在，则执行删除操作。删除成功后，检查数据库中是否还有引用此资源的记录，
	 * 如无引用则物理删除文件。过程中捕获的异常会被转换为自定义异常反馈。
	 *
	 * @param courseID         课程ID，用于定位课程，类型为Long。
	 * @param courseResourceId 要删除的课程资源ID，类型为Long。
	 * @param UUID             用户的唯一标识UUID，用于权限验证，类型为String。
	 */
	@SneakyThrows
	@Override
	public void deleteCourseResource (Long courseID, Long courseResourceId, String UUID) {
		try {
			// 验证用户是否为课程管理员
			User user = VerifyCourseMaster(courseID, UUID);
			
			// 根据ID获取课程资源
			com.example.deepschool.Model.CourseResource courseResource = this.courseResourceRepository.select(courseResourceId);
			if (courseResource == null) {
				throw new RuntimeException("该资源不存在");
			}
			
			// 尝试删除课程资源
			if (this.courseResourceRepository.delete(courseResourceId) == 0) {
				throw new RuntimeException("删除失败");
			}
			
			// 如果没有其他记录引用此资源地址，则物理删除文件
			if (this.courseResourceRepository.selectCountByAddress(courseResource.getAddress()) == 0) {
				IO.deleteFile(GlobalVariables_String.File_Path_CourseResource.getValue() + courseResource.getAddress());
			}
		} catch (Exception e) {
			// 处理特定类型的异常并转换为自定义异常
			if (e.getCause() != null && e.getCause().toString().startsWith("java.sql.SQL")) {
				throw new CustomException(500, "服务器异常.请联系管理员.");
			}
			throw new CustomException(400, e.getMessage());
		}
	}
	
	/**
	 * 获取指定课程的所有资源列表。
	 * <p>
	 * 首先验证用户是否拥有指定课程的访问权限，之后查询并返回该课程下所有资源的列表。
	 * 如果发生异常，特别是与数据库操作相关的异常，将被转换为自定义异常反馈。
	 *
	 * @param courseId 目标课程的ID，用于查询资源列表，类型为Long。
	 * @param UUID     用户的唯一标识UUID，用于权限验证，类型为String。
	 * @return 返回一个数组，包含课程资源的实体对象 {@link com.example.deepschool.Model.CourseResource}。
	 */
	@SneakyThrows
	@Override
	public com.example.deepschool.Model.CourseResource[] CourseResourceByCourseId (Long courseId, String UUID) {
		try {
			// 验证用户是否拥有课程访问权限
			isOwnedCourse(courseId, UUID);
			
			// 查询并返回课程资源列表
			return this.courseResourceRepository.selectByCourseId(courseId);
		} catch (Exception e) {
			// 特定异常处理并转换为自定义异常
			if (e.getCause() != null && e.getCause().toString().startsWith("java.sql.SQL")) {
				throw new CustomException(500, "服务器异常.请联系管理员.");
			}
			throw new CustomException(400, e.getMessage());
		}
	}
	
	
	/**
	 * 上传文件块并处理合并逻辑。
	 * <p>
	 * 该方法首先验证用户是否为课程管理员，然后检查文件是否已完整存在。
	 * 对于新上传的块，进行完整性校验，包括块序号、SHA256校验值，并存储到Redis中。
	 * 当所有块上传完毕，进行块的合并，生成最终文件，并将其保存到课程资源目录。
	 * 成功后，将资源信息保存到数据库，并清理临时文件。过程中捕获的异常会被转换为自定义异常反馈。
	 *
	 * @param ChunkFile    当前上传的文件块，类型为MultipartFile。
	 * @param intactSHA256 完整文件的SHA256校验和，用于跟踪文件完整性，类型为String。
	 * @param ChunkSHA256  当前文件块的SHA256校验和，类型为String。
	 * @param ChunkNum     当前上传块的序号，类型为Long。
	 * @param MaxChunkNum  文件总块数，类型为Long。
	 * @param ResourceName 资源的名称，类型为String。
	 * @param courseID     课程ID，关联资源所属课程，类型为Long。
	 * @param UUID         用户的唯一标识UUID，用于权限验证，类型为String。
	 * @return 返回一个Map，包含上传状态("isComplete")、已上传块数("chunkNum")或资源地址("address")等信息。
	 */
	@SneakyThrows
	@Override
	public Map<String, Object> putChunk (MultipartFile ChunkFile,
	                                     String intactSHA256,
	                                     String ChunkSHA256,
	                                     Long ChunkNum,
	                                     Long MaxChunkNum,
	                                     String ResourceName,
	                                     Long courseID,
	                                     String UUID) {
		String tempPath = GlobalVariables_String.File_Path_Temp.getValue() + intactSHA256;
		
		try {
			// 验证用户是否为课程管理员
			User user = VerifyCourseMaster(courseID, UUID);
			
			// 检查文件是否已存在
			String address = Verify.VerifyFileIsExistCourseResource(intactSHA256);
			if (address != null) {
				return getStringObjectMap(ResourceName, courseID, address, new HashMap<>());
			}
			
			// 校验块序号和SHA256
			Long oldChunkNum = Redis.LLEN(intactSHA256);
			if (! Objects.equals(oldChunkNum, ChunkNum)) {
				throw new RuntimeException("ChunkNum错误");
			}
			String SHA_256 = SHA256.calculateFileSHA256(ChunkFile);
			if (! Objects.equals(SHA_256, ChunkSHA256)) {
				throw new RuntimeException("ChunkSHA256错误");
			}
			
			// 文件块处理逻辑
			if (oldChunkNum == 0) {
				IO.createFolder(tempPath);
			}
			IO.writeFile(ChunkFile, tempPath + "/" + ChunkSHA256);
			oldChunkNum = Redis.LPUSH(intactSHA256, SHA_256);
			
			// 检查是否所有块已上传
			if (! Objects.equals(oldChunkNum, MaxChunkNum)) {
				Map<String, Object> map = new HashMap<>();
				map.put("isComplete", false);
				map.put("chunkNum", oldChunkNum);
				return map;
			}
			
			// 块合并逻辑
			String[] ChunkSHA256s = new String[MaxChunkNum.intValue()];
			for (int i = 0; i < MaxChunkNum; i++) {
				ChunkSHA256s[i] = Redis.RPOP(intactSHA256);
			}
			String intactSHA256_ = SHA256.calculateIncrementalSHA256(ChunkSHA256s);
			if (! Objects.equals(intactSHA256, intactSHA256_)) {
				throw new RuntimeException("intactSHA256错误");
			}
			String writePath = GlobalVariables_String.File_Path_CourseResource.getValue() + intactSHA256;
			address = IO.mergeFile(tempPath + "/", ChunkSHA256s, writePath, intactSHA256_);
			IO.deleteTempFolder(tempPath);
			
			// 保存资源信息到数据库并返回结果
			return getStringObjectMap(ResourceName, courseID, address, new HashMap<>());
		} catch (Exception e) {
			// 特定异常处理
			if (e.getMessage().startsWith("intactSHA256")) {
				IO.deleteTempFolder(tempPath);
			}
			if (e.getCause() != null && e.getCause().toString().startsWith("java.sql.SQL")) {
				throw new CustomException(500, "服务器异常.请联系管理员.");
			}
			if ("文件上传失败".equals(e.getMessage())) {
				Redis.LPOP(intactSHA256);
			}
			throw new CustomException(400, e.getMessage());
		}
	}
	
	
	/**
	 * 生成临时下载链接供课程资源使用。
	 * <p>
	 * 此方法验证用户是否拥有指定课程的权限，然后创建一个具有有效期限的临时唯一标识（TempUUID），
	 * 将资源相关信息存储至Redis，并返回一个包含临时下载链接及有效时间的Map。
	 *
	 * @param courseId     课程ID，用于权限验证，类型为Long。
	 * @param ResourceName 资源的名称，用于Redis存储，类型为String。
	 * @param address      资源的实际存储地址，同样存储至Redis，类型为String。
	 * @param UU_ID        用户唯一标识，用于权限验证，类型为String。
	 * @return 返回一个Map，其中包含生成的临时下载链接("url")和该链接的有效截止时间("ValidTime")。
	 */
	@SneakyThrows
	@Override
	public Map<String, Object> TempDownloadLink (Long courseId, String ResourceName, String address, String UU_ID) {
		try {
			// 验证用户是否拥有课程访问权限
			isOwnedCourse(courseId, UU_ID);
			
			// 设置临时链接有效期为12小时
			long ValidTime = 60 * 60 * 12;
			
			// 生成随机的TempUUID作为键
			String TempUUID = UUID.randomUUID().toString();
			
			// 在Redis中存储资源信息
			Redis.HSET(TempUUID, "host", "0");
			Redis.HSET(TempUUID, "address", address);
			Redis.HSET(TempUUID, "name", ResourceName);
			Redis.EXPIRE(TempUUID, ValidTime);
			
			// 准备返回的Map数据
			Map<String, Object> map = new HashMap<>();
			String url = GlobalVariables_String.Host.getValue() + "TempDownload?TempUUID=" + TempUUID;
			map.put("url", url);
			map.put("ValidTime", LocalDateTime.now().plusHours(12));
			
			// 返回包含下载链接和有效时间的Map
			return map;
		} catch (Exception e) {
			// 特定异常处理
			if (e.getCause() != null && e.getCause().toString().startsWith("java.sql.SQL")) {
				throw new CustomException(500, "服务器异常.请联系管理员.");
			}
			throw new CustomException(400, e.getMessage());
		}
	}
	
}
